﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Globalization;
using System.Net;
using System.Text.RegularExpressions;

namespace Laharsub.Client
{
    class HttpLongPollManager
    {
        bool pollAborted;
        HttpWebRequest poll;
        Dictionary<int, Subscription> subscriptions;
        string baseAddress;
        object syncRoot = new object();
        AsyncCallback onPollResponse;
        string connectionGroupName;

        static Regex contentTypeRegex = new Regex(@"Content-type:\s*(.+)", RegexOptions.IgnoreCase);
        static Regex contentDescriptionRegex = new Regex(@"Content-Description:\s*(\d+)/(\d+)\r\n", RegexOptions.IgnoreCase);

        public HttpLongPollManager(string baseAddress)
        {
            this.baseAddress = baseAddress;
            this.subscriptions = new Dictionary<int, Subscription>();
            this.onPollResponse = new AsyncCallback(this.OnPollResponse);
            this.connectionGroupName = Guid.NewGuid().ToString();
        }

        void AbortPoll()
        {
            if (this.poll != null)
            {
                lock (this.syncRoot)
                {
                    if (this.poll != null)
                    {
                        this.pollAborted = true;
                        this.poll.Abort();
                        this.poll = null;
                    }
                }
            }
        }

        public void AddSubscription(Subscription s)
        {
            lock (this.syncRoot)
            {
                if (this.subscriptions.ContainsKey(s.TopicId))
                {
                    throw new InvalidOperationException("Cannot create subscription. Only one subscription to a given topic may be active at a time.");
                }
                this.subscriptions[s.TopicId] = s;
                this.AbortPoll();
                this.StartPoll();
            }
        }

        public void RemoveSubscription(int topicId)
        {
            lock (this.syncRoot)
            {
                if (this.subscriptions.ContainsKey(topicId))
                {
                    this.subscriptions.Remove(topicId);
                    if (this.subscriptions.Count == 0)
                    {
                        this.AbortPoll();
                    }
                }
            }
        }

        public void RemoveAllSubscriptions()
        {
            lock (this.syncRoot)
            {
                this.AbortPoll();
                this.subscriptions.Clear();
            }
        }

        void FaultAllSubscriptions(Exception e)
        {
            lock (this.syncRoot)
            {
                Exception e1 = new InvalidOperationException("HTTP long poll returned an error. Subscription is terminated.", e);
                foreach (Subscription s in this.subscriptions.Values)
                {
                    if (s.OnError != null)
                    {
                        try
                        {
                            s.OnError(s, e1);
                        }
                        catch (Exception)
                        {
                            // empty
                        }
                    }
                }
                this.subscriptions.Clear();
                this.AbortPoll();
            }
        }

        void StartPoll()
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendFormat(CultureInfo.InvariantCulture, "{0}subscriptions/volatile?", this.baseAddress);
            int i = 0;
            foreach (Subscription s in this.subscriptions.Values)
            {
                if (i > 0)
                {
                    sb.Append("&");
                }
                sb.AppendFormat(CultureInfo.InvariantCulture, "subs[{0}][topicid]={1}", i, s.TopicId);
                if (s.From > 0)
                {
                    sb.AppendFormat(CultureInfo.InvariantCulture, "&subs[{0}][from]={1}", i, s.From);
                }
                i++;
            }
            this.pollAborted = false;
            this.poll = (HttpWebRequest)WebRequest.Create(sb.ToString());
#if SILVERLIGHT
#else
            this.poll.Pipelined = false;
            this.poll.ConnectionGroupName = this.connectionGroupName;
#endif
            this.poll.BeginGetResponse(this.onPollResponse, null);
        }

        void OnPollResponse(IAsyncResult result)
        {            
            lock (this.syncRoot) // prevent race condition with OnAbort
            {
                if (this.poll != null && ! this.pollAborted)
                {
                    bool success = false;
                    HttpWebResponse response = null;
                    try
                    {
                        response = (HttpWebResponse)this.poll.EndGetResponse(result);
                        Stream stream = response.GetResponseStream();
                        // TODO: implement asynchronous multipart/mixed parsing without response buffering
                        string body = this.ReadEntireStream(stream);
                        if (!string.IsNullOrEmpty(body))
                        {
                            this.ParseMultipartMime(body);
                        }
                        success = true;
                    }
                    catch (Exception e)
                    {
                        this.FaultAllSubscriptions(e);
                    }
                    finally
                    {
                        this.poll = null;
                        if (response != null)
                        {
                            response.Close();
                        }
                    }
                    if (success && this.subscriptions.Count > 0)
                    {
                        this.StartPoll();
                    }
                }
            }
        }

        string ReadEntireStream(Stream s)
        {
            StringBuilder sb = new StringBuilder();
            byte[] buffer = new byte[256];
            int i;
            while ((i = s.Read(buffer, 0, buffer.Length)) > 0)
            {
                sb.Append(Encoding.UTF8.GetString(buffer, 0, i));
            }
            return sb.ToString();
        }

        void ParseMultipartMime(string result)
        {
            int i = result.IndexOf("\r\n");
            if (i < 3)
            {
                throw new InvalidOperationException("Cannot determine multipart/mixed boundary. Malformed HTTP long poll response.");
            }
            string boundary = result.Substring(0, i);

            int mimePartIndex = 0;
            while (result.Length > mimePartIndex)
            {
                int boundaryIndex = result.IndexOf(boundary, mimePartIndex);
                if (boundaryIndex == -1)
                {
                    break;
                }
                if (mimePartIndex > 0)
                {
                    string mimePart = result.Substring(mimePartIndex, boundaryIndex - mimePartIndex - 2);
                    this.ParseMimePart(mimePart);
                }
                mimePartIndex = boundaryIndex + boundary.Length + 2;
            }
        }

        void ParseMimePart(string part)
        {
            PubsubMessage message = new PubsubMessage();

            int i = part.IndexOf("\r\n");
            if (i <= 0)
            {
                throw new InvalidOperationException("Cannot determine the content type of the MIME part.");
            }
            Match m = contentTypeRegex.Match(part, 0, i);
            if (!m.Success)
            {
                throw new InvalidOperationException("Cannot determine the content type of the MIME part.");
            }
            message.ContentType = m.Groups[1].Value;

            m = contentDescriptionRegex.Match(part, i);
            if (!m.Success)
            {
                throw new InvalidOperationException("Connot determine topicId and messageId of the MIME part.");
            }
            message.TopicId = int.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture);
            message.MessageId = int.Parse(m.Groups[2].Value, CultureInfo.InvariantCulture);

            message.Body = new MemoryStream(Encoding.UTF8.GetBytes(part.Substring(part.IndexOf("\r\n\r\n") + "\r\n\r\n".Length)));

            this.DispatchMessage(message);
        }

        // TODO: should callback be dispatched on a separate thread? Dispatching on the poll response thread slows down issuance of next poll.
        void DispatchMessage(PubsubMessage message)
        {
            Subscription s = null;
            if (this.subscriptions.TryGetValue(message.TopicId, out s))
            {
                if (s.From <= message.MessageId)
                {
                    s.From = message.MessageId + 1;
                }
                try
                {
                    if (s.OnMessageReceived != null)
                    {
                        s.OnMessageReceived(s, message);
                    }
                }
                catch (Exception)
                {
                    // empty
                }
            }
        }
    }
}
